package com.ls.blog.aop;

import com.ls.blog.entity.Log;
import com.ls.blog.service.LogService;
import com.ls.blog.utils.IpUtil;
import com.ls.blog.utils.UserUtils;
import io.swagger.annotations.ApiOperation;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import java.time.LocalDateTime;
import java.util.Objects;
import java.util.concurrent.ExecutorService;


@Aspect
@Component
public class LogAspect {


    private static final ThreadLocal<Long> beginTime = new ThreadLocal<>();


    private static final ThreadLocal<Log> logEntity = new ThreadLocal<>();



    @Autowired
    HttpServletRequest httpServletRequest;

    @Autowired
    ExecutorService executorService;

    @Autowired
    LogService logService;


    /**
     * controller切入点
     */
    @Pointcut("execution(* com.ls.blog.controller.*.*(..))")
    public void pointCut(){

    }


    /**
     * 用户登录切入点
     */
    @Pointcut("execution(* com.ls.blog.service.impl.UserDetailsServiceImpl.loadUserByUsername(String))")
    public void loginCut(){

    }

    /**
     * 前置通知，目标方法调用前被调用
     */
    @Before("pointCut()")
    public void beforeAdvice(JoinPoint joinPoint){
        if (isRequiredLog(joinPoint)){
            return;
        }
        // 设置开始时间
        beginTime.set(System.currentTimeMillis());
    }

    /**
     * 最终通知，目标方法执行完之后执行
     */
    @After("pointCut()")
    public void afterAdvice(){
        // 删除threadLocal
        beginTime.remove();
        logEntity.remove();
    }

    /**
     * 后置返回通知
     * 如果参数中的第一个参数为JoinPoint，则第二个参数为返回值的信息
     * 如果参数中的第一个参数不为JoinPoint，则第一个参数为returning中对应的参数
     * returning 只有目标方法返回值与通知方法相应参数类型时才能执行后置返回通知，否则不执行
     * @param joinPoint
     */
    @AfterReturning(value = "execution(* com.ls.blog.controller.*.*(..))")
    public void afterReturningAdvice(JoinPoint joinPoint){
        if (isRequiredLog(joinPoint)){
            return;
        }
       logEntity.set(Log
               .builder()
               .time(System.currentTimeMillis()-beginTime.get())
               .build()
       );
    }

    /**
     * 后置异常通知
     * 定义一个名字，该名字用于匹配通知实现方法的一个参数名，当目标方法抛出异常返回后，将把目标方法抛出的异常传给通知方法；
     * throwing 只有目标方法抛出的异常与通知方法相应参数异常类型时才能执行后置异常通知，否则不执行，
     * @param joinPoint
     * @param e
     */
    @AfterThrowing(value = "pointCut()", throwing = "e")
    public void afterThrowingAdvice(JoinPoint joinPoint, Exception e){
        if (isRequiredLog(joinPoint)){
            return;
        }
        logEntity.set(Log
                .builder()
                .describle("出现异常，异常信息为:"+e.getMessage())
                .build()
        );
    }

    /**
     * 环绕通知
     * 环绕通知非常强大，可以决定目标方法是否执行，什么时候执行，执行时是否需要替换方法参数，执行完毕是否需要替换返回值。
     * 环绕通知第一个参数必须是org.aspectj.lang.ProceedingJoinPoint类型
     * @param proceedingJoinPoint
     */
    @Around("pointCut()")
    public Object aroundAdvice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        // 执行方法前
        long start = System.currentTimeMillis();
        Object result = proceedingJoinPoint.proceed();
        MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();
        LogAop annotation = signature.getMethod().getAnnotation(LogAop.class);
        if (Objects.nonNull(annotation)){
            // 执行方法后
            long end = System.currentTimeMillis();
            // 封装日志信息
            String typeName = signature.getDeclaringTypeName();
            ApiOperation  apiOperation = signature.getMethod().getAnnotation(ApiOperation.class);
            String value = apiOperation.value();
            String name = signature.getName();
            Log log = Log.builder()
                    .method(typeName + ":" + name)
                    .username(Objects.nonNull(UserUtils.getCurrentUser()) ? UserUtils.getCurrentUser().getUsername() : "匿名用户")
                    .ip(IpUtil.getIpAddr(httpServletRequest))
                    .time(end-start)
                    .describle(value)
                    .date(LocalDateTime.now())
                    .build();
            // 线程池提交任务
            executorService.submit(new LogTask(logService,log));
        }
        return result;
    }


    // 登录环绕通知
    @Around("loginCut()")
   public Object loginAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        Object proceed = proceedingJoinPoint.proceed();
        Log log = Log.builder()
                .ip(IpUtil.getIpAddr(httpServletRequest))
                .describle("用户登录")
                .date(LocalDateTime.now())
                .build();
        executorService.submit(new LogTask(logService,log));
        return proceed;
    }

    /**
     * 判断是否需要记录日志
     * @return
     */
    public boolean isRequiredLog(JoinPoint joinPoint){
        LogAop annotation = joinPoint.getClass().getAnnotation(LogAop.class);
        Integer i = new Integer(1);
        return Objects.isNull(annotation);
    }
}




class LogTask implements Runnable{

    private final LogService logService;
    private final Log log;

    public LogTask(LogService logService, Log log) {
        this.logService = logService;
        this.log = log;
    }

    @Override
    public void run() {
        logService.save(log);
    }
}

